Содержание

  • 1  Подготовка
  • 2  Анализ
  • 3  Обучение
  • 4  Тестирование
  • 5  Вывод
  • 6  Чек-лист проверки

Прогнозирование заказов такси¶

Компания «Чётенькое такси» собрала исторические данные о заказах такси в аэропортах. Чтобы привлекать больше водителей в период пиковой нагрузки, нужно спрогнозировать количество заказов такси на следующий час. Постройте модель для такого предсказания.

Значение метрики RMSE на тестовой выборке должно быть не больше 48.

Вам нужно:

  1. Загрузить данные и выполнить их ресемплирование по одному часу.
  2. Проанализировать данные.
  3. Обучить разные модели с различными гиперпараметрами. Сделать тестовую выборку размером 10% от исходных данных.
  4. Проверить данные на тестовой выборке и сделать выводы.

Данные лежат в файле taxi.csv. Количество заказов находится в столбце num_orders (от англ. number of orders, «число заказов»).

Подготовка¶

In [1]:
import time
import warnings
import numpy as np
import pandas as pd
import lightgbm as lgb
import plotly.graph_objects as go

from lightgbm import LGBMRegressor
from IPython.display import display
from catboost import CatBoostRegressor
from sklearn.metrics import mean_squared_error
from sklearn.tree import DecisionTreeRegressor
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor
from statsmodels.tsa.seasonal import seasonal_decompose
from sklearn.model_selection import GridSearchCV, train_test_split, TimeSeriesSplit

warnings.filterwarnings('ignore')
In [2]:
try:
    df = pd.read_csv('taxi.csv', index_col=[0], parse_dates=[0]) 
except:
    df = pd.read_csv('/datasets/taxi.csv', index_col=[0], parse_dates=[0])
In [3]:
display(df.head(), df.shape)
num_orders
datetime
2018-03-01 00:00:00 9
2018-03-01 00:10:00 14
2018-03-01 00:20:00 28
2018-03-01 00:30:00 20
2018-03-01 00:40:00 32
(26496, 1)
In [4]:
df.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 26496 entries, 2018-03-01 00:00:00 to 2018-08-31 23:50:00
Data columns (total 1 columns):
 #   Column      Non-Null Count  Dtype
---  ------      --------------  -----
 0   num_orders  26496 non-null  int64
dtypes: int64(1)
memory usage: 414.0 KB
In [5]:
df.isna().sum()
Out[5]:
num_orders    0
dtype: int64
In [6]:
df.sort_index(inplace=True)
In [7]:
# Проверим индекс на монотонность
df.index.is_monotonic
Out[7]:
True
In [8]:
df = df.resample('1H').sum()
In [9]:
decomposed = seasonal_decompose(df)
decomposed_day = seasonal_decompose(df.resample('1D').sum())

Промежуточный вывод:

В разделе "Подготовка данных" были выполнены следующие задачи:

  1. Чтение данных и установка признака "datetime" в качестве индекса.
  2. Ресемплирование данных по одному часу.
  3. Создание новых признаков, включая календарные признаки (день недели и час), отстающие значения и скользящее среднее.

В результате выполнения этих задач было обнаружено, что даты и время в датафрейме расположены в хронологическом порядке. Этот факт подтверждает правильность выполнения предварительной обработки данных.

Таким образом, данные были подготовлены для обучения моделей.

Анализ¶

In [10]:
df.head()
Out[10]:
num_orders
datetime
2018-03-01 00:00:00 124
2018-03-01 01:00:00 85
2018-03-01 02:00:00 71
2018-03-01 03:00:00 66
2018-03-01 04:00:00 43
In [11]:
def plotly(data, title):
    fig = go.Figure(data=go.Scatter(x=data.index, y=data))
    fig.update_layout(title=title, plot_bgcolor='white')
    fig.show()
In [12]:
plotly(decomposed.trend, 'Trend')
In [13]:
plotly(decomposed.trend.rolling(24*7).mean(), 'Trend (smoothed)')
In [14]:
plotly(decomposed.seasonal.tail(24*7), 'Seasonality')
In [15]:
plotly(decomposed_day.seasonal['2018-03-01':'2018-03-15'], 'Weekly seasonality')
In [16]:
plotly(decomposed.seasonal['2018-03-01':'2018-03-2'], 'Daily seasonality')

Промежуточный вывод:

Из анализа данных можно сделать следующие выводы:

  • Присутствует общий тренд равномерного роста количества заказов с периодическими небольшими падениями. Это может указывать на постепенное увеличение спроса или развитие бизнеса в данной области.
  • Наблюдается сезонность по дням, где заказы достигают минимума в ночное время, затем возрастают с утра и в течение дня, достигая пика к вечеру. Это может указывать на влияние времени суток и поведения потребителей на количество заказов.

Оба этих наблюдения могут быть полезны при прогнозировании и планировании заказов в будущем, а также при принятии решений по оптимизации и управлению предприятием.

Обучение¶

In [17]:
def make_features(data):
    data = df.copy()
    data['month'] = df.index.month
    data['day'] = df.index.day
    data['dayofweek'] = df.index.dayofweek
    data['hour'] = df.index.hour
    
    for i in range(1, 6):
        data['lag_{}'.format(i)] = data['num_orders'].shift(i)
    
    data['rolling_mean'] = data['num_orders'].shift().rolling(1).mean()
    data.dropna(inplace=True)
    
    return data
In [18]:
data = make_features(df)
In [19]:
data.head()
Out[19]:
num_orders month day dayofweek hour lag_1 lag_2 lag_3 lag_4 lag_5 rolling_mean
datetime
2018-03-01 05:00:00 6 3 1 3 5 43.0 66.0 71.0 85.0 124.0 43.0
2018-03-01 06:00:00 12 3 1 3 6 6.0 43.0 66.0 71.0 85.0 6.0
2018-03-01 07:00:00 15 3 1 3 7 12.0 6.0 43.0 66.0 71.0 12.0
2018-03-01 08:00:00 34 3 1 3 8 15.0 12.0 6.0 43.0 66.0 15.0
2018-03-01 09:00:00 69 3 1 3 9 34.0 15.0 12.0 6.0 43.0 34.0
In [20]:
features = data.drop(['num_orders'], axis=1)
target = data['num_orders']
In [21]:
features_train, features_test, \
target_train, target_test = train_test_split(features, target, shuffle=False, test_size=0.1, random_state=12345,)
In [22]:
results_df = pd.DataFrame(columns=['Model', 'Training Time', 'Prediction Time', 'RMSE Train'])
In [23]:
tscv = TimeSeriesSplit(n_splits=5)
In [24]:
def fit_model(estimator, param_grid, features_train, target_train, features_test, target_test):
    model = GridSearchCV(estimator=estimator,
                         param_grid=param_grid,
                         n_jobs=-1,
                         cv=tscv,
                         scoring='neg_root_mean_squared_error'
                         )

    start_time = time.time()
    model.fit(features_train, target_train)
    training_time = time.time() - start_time

    best_rmse = abs(round(model.best_score_, 1))

    print(f'Best RMSE: {best_rmse}')
    print(f'Best params: {model.best_params_}')

    best_model = estimator.set_params(**model.best_params_)

    start_time = time.time()
    best_model.fit(features_train, target_train)
    prediction_time = time.time() - start_time

    predictions_train = best_model.predict(features_train)
    train_rmse = mean_squared_error(target_train, predictions_train, squared=False)

    return best_model, best_rmse, training_time, prediction_time, train_rmse
In [25]:
best_model, best_rmse, training_time, prediction_time, train_rmse_DTR = fit_model(DecisionTreeRegressor(random_state=12345), {'max_depth': range(1, 11, 2)}, features_train, target_train, features_test, target_test)

results_df.loc[0] = ['DecisionTreeRegressor', training_time, prediction_time, train_rmse_DTR]

results_df
Best RMSE: 29.3
Best params: {'max_depth': 7}
Out[25]:
Model Training Time Prediction Time RMSE Train
0 DecisionTreeRegressor 0.184136 0.011271 22.023192
In [26]:
best_model, best_rmse, training_time, \
prediction_time, train_rmse_RFR = fit_model(RandomForestRegressor(random_state=12345), 
            {'n_estimators': range(50, 100, 10), 'max_depth': range(1, 11, 2)}, 
            features_train, target_train, features_test, target_test)

results_df.loc[1] = ['RandomForestRegressor', training_time, prediction_time, train_rmse_RFR]

results_df
Best RMSE: 25.9
Best params: {'max_depth': 9, 'n_estimators': 80}
Out[26]:
Model Training Time Prediction Time RMSE Train
0 DecisionTreeRegressor 0.184136 0.011271 22.023192
1 RandomForestRegressor 30.145162 0.783082 17.018848
In [32]:
best_model, best_rmse, training_time, \
prediction_time, train_rmse_LR = fit_model(LinearRegression(), 
            [{'fit_intercept': [True, False]},
             {'copy_X': [True, False]}, {'n_jobs': [1, -1]}], 
            features_train, target_train, features_test, target_test)

results_df.loc[2] = ['LinearRegression', training_time, prediction_time, train_rmse_LR]

results_df
Best RMSE: 31.5
Best params: {'fit_intercept': True}
Out[32]:
Model Training Time Prediction Time RMSE Train
0 DecisionTreeRegressor 0.184136 0.011271 22.023192
1 RandomForestRegressor 30.145162 0.783082 17.018848
3 CatBoostRegressor 63.270962 0.755724 14.432441
4 LGBMRegressor 1.886202 0.036697 17.952839
2 LinearRegression 0.116830 0.002021 30.504124
In [33]:
best_model, best_rmse, training_time, \
prediction_time, train_rmse_LR = fit_model(CatBoostRegressor(random_state=12345, verbose=False), 
            {'depth': [4, 6, 8], 'learning_rate': [0.01, 0.1, 1]}, 
            features_train, target_train, features_test, target_test)

results_df.loc[3] = ['CatBoostRegressor', training_time, prediction_time, train_rmse_LR]

results_df
Best RMSE: 25.5
Best params: {'depth': 4, 'learning_rate': 0.1}
Out[33]:
Model Training Time Prediction Time RMSE Train
0 DecisionTreeRegressor 0.184136 0.011271 22.023192
1 RandomForestRegressor 30.145162 0.783082 17.018848
3 CatBoostRegressor 63.522635 0.752388 14.432441
4 LGBMRegressor 1.886202 0.036697 17.952839
2 LinearRegression 0.116830 0.002021 30.504124
In [34]:
model = LGBMRegressor(random_state=12345)

# Определяем сетку гиперпараметров для настройки
parameters = {'n_estimators': [50, 100, 200],
              'max_depth': [3, 5, 7]}

# Выполняем поиск по сетке с использованием перекрестной проверки
best_model, best_rmse, training_time_LGBM, prediction_time_LGBM, train_rmse_LGBM = fit_model(model, parameters, features_train, target_train, features_test, target_test)

# Добавляем результаты текущей модели в фреймворк данных
results_df.loc[4] = ['LGBMRegressor', training_time_LGBM, prediction_time_LGBM, train_rmse_LGBM]

# Показываем результаты
results_df
Best RMSE: 25.2
Best params: {'max_depth': 7, 'n_estimators': 50}
Out[34]:
Model Training Time Prediction Time RMSE Train
0 DecisionTreeRegressor 0.184136 0.011271 22.023192
1 RandomForestRegressor 30.145162 0.783082 17.018848
3 CatBoostRegressor 63.522635 0.752388 14.432441
4 LGBMRegressor 1.857874 0.036788 17.952839
2 LinearRegression 0.116830 0.002021 30.504124

Промежуточный вывод:

Была проведена разбивка данных на выборки. Создана функция fit_model(), которая использует метод GridSearchCV для подбора наилучшей модели и вывода метрики RMSE и параметров лучшей модели. Были обучены пять модели DecisionTreeRegressor, RandomForestRegressor, LinearRegression, CatBoostRegressor, LGBMRegressor). Были выведены метрики RMSE для каждой из этих моделей для тренировачной и тестовой в виде таблицы.

По результатам мы видим, что лучшей моделью стала CatBoostRegressor и LGBMRegressor = 43.657045

Тестирование¶

In [36]:
def display_result(target, pred, rmse, model_name):
    
    # Преобразуем целевую переменную в DataFrame и сбрасываем индекс
    result = target.to_frame().reset_index()
    
    # Добавляем столбец с предсказаниями в DataFrame
    result['prediction'] = pd.Series(pred)
    
    # Устанавливаем столбец datetime в качестве индекса
    result.set_index('datetime', inplace=True)

    fig = go.Figure()
    
    # Добавляем график с реальными значениями
    fig.add_trace(go.Scatter(x=result.index, y=result[target.name], name='True'))
    
     # Добавляем график с предсказанными значениями
    fig.add_trace(go.Scatter(x=result.index, y=result['prediction'], name='Predicted'))
    
    # Устанавливаем заголовок графика и подписи осей
    fig.update_layout(title=model_name + ' (RMSE: ' + str(rmse) + ')', xaxis_title='Время (дни)', yaxis_title='Количество заказов', plot_bgcolor='white')
    
    # Отображаем график
    display(fig)
In [37]:
for i in range(len(results_df)):
    model_name = results_df.loc[i, 'Model']
    
    if model_name == 'CatBoostRegressor':
        model = CatBoostRegressor(verbose=False)
        
        # Подгоняем модель к обучающим данным
        model.fit(features_train, target_train)
        
        # Спрогнозируем на тестовых данных
        predictions = model.predict(features_test)
        
        # Рассчитаем среднеквадратичное отклонение
        rmse = mean_squared_error(target_test, predictions, squared=False)
        
        # Выводим результаты
        display_result(target_test, predictions, rmse, model_name)

Вывод¶

Было проведено исследование, направленное на создание модели машинного обучения для прогнозирования количества заказов такси в аэропортах на следующий час. Это поможет привлечь больше водителей в периоды пиковой нагрузки. Входные данные для модели - исторические данные о заказах такси в аэропортах с 1 марта по 31 августа 2018 года. Результаты на обучающей выборке показали, что удалось создать модель для успешного прогнозирования, таковыми являются CatBoostRegressor и LGBMRegressor

Дополнительный материал

Полезная лекция про временные ряды: https://www.youtube.com/watch?v=u433nrxdf5k

Б.Б. Демешев - временные ряды https://disk.yandex.ru/i/LiDHB-B3A6Lz5A

Базовое применение ARIMA - https://colab.research.google.com/drive/17RnG91Eq8JBKyxToNzvCvjibfxum-oPj?usp=sharing

Канторович - Анализ временных рядов https://yadi.sk/i/IOkUOS3hTXf3gg https://facebook.github.io/prophet/

https://facebook.github.io/prophet/docs/quick_start.html#python-api

https://nbviewer.jupyter.org/github/miptgirl/habra_materials/blob/master/prophet/habra_data.ipynb

Чек-лист проверки¶

  • Jupyter Notebook открыт
  • Весь код выполняется без ошибок
  • Ячейки с кодом расположены в порядке исполнения
  • Данные загружены и подготовлены
  • Данные проанализированы
  • Модель обучена, гиперпараметры подобраны
  • Качество моделей проверено, выводы сделаны
  • Значение RMSE на тестовой выборке не больше 48